/**
 * @license
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import '../../../scripts/bundled-polymer.js';

import '../../../behaviors/fire-behavior/fire-behavior.js';
import '../../../behaviors/gr-access-behavior/gr-access-behavior.js';
import '@polymer/paper-toggle-button/paper-toggle-button.js';
import '../../../styles/gr-form-styles.js';
import '../../../styles/gr-menu-page-styles.js';
import '../../../styles/shared-styles.js';
import '../../shared/gr-autocomplete/gr-autocomplete.js';
import '../../shared/gr-button/gr-button.js';
import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
import '../gr-rule-editor/gr-rule-editor.js';
import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
import {htmlTemplate} from './gr-permission_html.js';

const MAX_AUTOCOMPLETE_RESULTS = 20;

const RANGE_NAMES = [
  'QUERY LIMIT',
  'BATCH CHANGES LIMIT',
];

/**
 * @appliesMixin Gerrit.AccessMixin
 * @appliesMixin Gerrit.FireMixin
 */
/**
 * Fired when the permission has been modified or removed.
 *
 * @event access-modified
 */
/**
 * Fired when a permission that was previously added was removed.
 *
 * @event added-permission-removed
 * @extends Polymer.Element
 */
class GrPermission extends mixinBehaviors( [
  Gerrit.AccessBehavior,
  /**
   * Unused in this element, but called by other elements in tests
   * e.g gr-access-section_test.
   */
  Gerrit.FireBehavior,
], GestureEventListeners(
    LegacyElementMixin(
        PolymerElement))) {
  static get template() { return htmlTemplate; }

  static get is() { return 'gr-permission'; }

  static get properties() {
    return {
      labels: Object,
      name: String,
      /** @type {?} */
      permission: {
        type: Object,
        observer: '_sortPermission',
        notify: true,
      },
      groups: Object,
      section: String,
      editing: {
        type: Boolean,
        value: false,
        observer: '_handleEditingChanged',
      },
      _label: {
        type: Object,
        computed: '_computeLabel(permission, labels)',
      },
      _groupFilter: String,
      _query: {
        type: Function,
        value() {
          return this._getGroupSuggestions.bind(this);
        },
      },
      _rules: Array,
      _groupsWithRules: Object,
      _deleted: {
        type: Boolean,
        value: false,
      },
      _originalExclusiveValue: Boolean,
    };
  }

  static get observers() {
    return [
      '_handleRulesChanged(_rules.splices)',
    ];
  }

  /** @override */
  created() {
    super.created();
    this.addEventListener('access-saved',
        () => this._handleAccessSaved());
  }

  /** @override */
  ready() {
    super.ready();
    this._setupValues();
  }

  _setupValues() {
    if (!this.permission) { return; }
    this._originalExclusiveValue = !!this.permission.value.exclusive;
    flush();
  }

  _handleAccessSaved() {
    // Set a new 'original' value to keep track of after the value has been
    // saved.
    this._setupValues();
  }

  _permissionIsOwnerOrGlobal(permissionId, section) {
    return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES';
  }

  _handleEditingChanged(editing, editingOld) {
    // Ignore when editing gets set initially.
    if (!editingOld) { return; }
    // Restore original values if no longer editing.
    if (!editing) {
      this._deleted = false;
      delete this.permission.value.deleted;
      this._groupFilter = '';
      this._rules = this._rules.filter(rule => !rule.value.added);
      for (const key of Object.keys(this.permission.value.rules)) {
        if (this.permission.value.rules[key].added) {
          delete this.permission.value.rules[key];
        }
      }

      // Restore exclusive bit to original.
      this.set(['permission', 'value', 'exclusive'],
          this._originalExclusiveValue);
    }
  }

  _handleAddedRuleRemoved(e) {
    const index = e.model.index;
    this._rules = this._rules.slice(0, index)
        .concat(this._rules.slice(index + 1, this._rules.length));
  }

  _handleValueChange() {
    this.permission.value.modified = true;
    // Allows overall access page to know a change has been made.
    this.dispatchEvent(
        new CustomEvent('access-modified', {bubbles: true, composed: true}));
  }

  _handleRemovePermission() {
    if (this.permission.value.added) {
      this.dispatchEvent(new CustomEvent(
          'added-permission-removed', {bubbles: true, composed: true}));
    }
    this._deleted = true;
    this.permission.value.deleted = true;
    this.dispatchEvent(
        new CustomEvent('access-modified', {bubbles: true, composed: true}));
  }

  _handleRulesChanged(changeRecord) {
    // Update the groups to exclude in the autocomplete.
    this._groupsWithRules = this._computeGroupsWithRules(this._rules);
  }

  _sortPermission(permission) {
    this._rules = this.toSortedArray(permission.value.rules);
  }

  _computeSectionClass(editing, deleted) {
    const classList = [];
    if (editing) {
      classList.push('editing');
    }
    if (deleted) {
      classList.push('deleted');
    }
    return classList.join(' ');
  }

  _handleUndoRemove() {
    this._deleted = false;
    delete this.permission.value.deleted;
  }

  _computeLabel(permission, labels) {
    if (!labels || !permission ||
        !permission.value || !permission.value.label) { return; }

    const labelName = permission.value.label;

    // It is possible to have a label name that is not included in the
    // 'labels' object. In this case, treat it like anything else.
    if (!labels[labelName]) { return; }
    const label = {
      name: labelName,
      values: this._computeLabelValues(labels[labelName].values),
    };
    return label;
  }

  _computeLabelValues(values) {
    const valuesArr = [];
    const keys = Object.keys(values)
        .sort((a, b) => parseInt(a, 10) - parseInt(b, 10));

    for (const key of keys) {
      let text = values[key];
      if (!text) { text = ''; }
      // The value from the server being used to choose which item is
      // selected is in integer form, so this must be converted.
      valuesArr.push({value: parseInt(key, 10), text});
    }
    return valuesArr;
  }

  /**
   * @param {!Array} rules
   * @return {!Object} Object with groups with rues as keys, and true as
   *    value.
   */
  _computeGroupsWithRules(rules) {
    const groups = {};
    for (const rule of rules) {
      groups[rule.id] = true;
    }
    return groups;
  }

  _computeGroupName(groups, groupId) {
    return groups && groups[groupId] && groups[groupId].name ?
      groups[groupId].name : groupId;
  }

  _getGroupSuggestions() {
    return this.$.restAPI.getSuggestedGroups(
        this._groupFilter,
        MAX_AUTOCOMPLETE_RESULTS)
        .then(response => {
          const groups = [];
          for (const key in response) {
            if (!response.hasOwnProperty(key)) { continue; }
            groups.push({
              name: key,
              value: response[key],
            });
          }
          // Does not return groups in which we already have rules for.
          return groups
              .filter(group => !this._groupsWithRules[group.value.id]);
        });
  }

  /**
   * Handles adding a skeleton item to the dom-repeat.
   * gr-rule-editor handles setting the default values.
   */
  _handleAddRuleItem(e) {
    // The group id is encoded, but have to decode in order for the access
    // API to work as expected.
    const groupId = decodeURIComponent(e.detail.value.id)
        .replace(/\+/g, ' ');
    // We cannot use "this.set(...)" here, because groupId may contain dots,
    // and dots in property path names are totally unsupported by Polymer.
    // Apparently Polymer picks up this change anyway, otherwise we should
    // have looked at using MutableData:
    // https://polymer-library.polymer-project.org/2.0/docs/devguide/data-system#mutable-data
    this.permission.value.rules[groupId] = {};

    // Purposely don't recompute sorted array so that the newly added rule
    // is the last item of the array.
    this.push('_rules', {
      id: groupId,
    });

    // Add the new group name to the groups object so the name renders
    // correctly.
    if (this.groups && !this.groups[groupId]) {
      this.groups[groupId] = {name: this.$.groupAutocomplete.text};
    }

    // Wait for new rule to get value populated via gr-rule-editor, and then
    // add to permission values as well, so that the change gets propogated
    // back to the section. Since the rule is inside a dom-repeat, a flush
    // is needed.
    flush();
    const value = this._rules[this._rules.length - 1].value;
    value.added = true;
    // See comment above for why we cannot use "this.set(...)" here.
    this.permission.value.rules[groupId] = value;
    this.dispatchEvent(
        new CustomEvent('access-modified', {bubbles: true, composed: true}));
  }

  _computeHasRange(name) {
    if (!name) { return false; }

    return RANGE_NAMES.includes(name.toUpperCase());
  }
}

customElements.define(GrPermission.is, GrPermission);
